Skip to content

Conversation

@alexeyzimarev
Copy link
Contributor

@alexeyzimarev alexeyzimarev commented Nov 23, 2025

User description


PR Type

Bug fix


Description

  • Fix aggregate version calculation for deleted streams

    • Changed OriginalVersion from computed property to stored field
    • Pass stream revision when loading aggregate events
  • Rename Position to Revision in StreamEvent for clarity

    • Updated all references across persistence and event store implementations
  • Add test for stream deletion and recreation scenario

  • Refactor type registration method to extension method


Diagram Walkthrough

flowchart LR
  A["Aggregate.Load<br/>receives version"] -->|"stores version"| B["OriginalVersion<br/>field"]
  B -->|"used for"| C["CurrentVersion<br/>calculation"]
  C -->|"ensures correct"| D["Optimistic<br/>concurrency"]
  E["StreamEvent.Position"] -->|"renamed to"| F["StreamEvent.Revision"]
  F -->|"passed to"| A
Loading

File Walkthrough

Relevant files
Bug fix
2 files
Aggregate.cs
Fix version tracking for deleted streams                                 
+6/-5     
AggregatePersistenceExtensions.cs
Pass stream revision when loading aggregate                           
+1/-1     
Enhancement
8 files
StreamEvent.cs
Rename Position property to Revision                                         
+2/-2     
TieredEventReader.cs
Update Position references to Revision                                     
+6/-6     
StateStoreFunctions.cs
Update Position references to Revision                                     
+1/-1     
TypeMappingsGenerator.cs
Refactor Register to extension method                                       
+2/-2     
ElasticEventStore.cs
Update Position references to Revision                                     
+1/-1     
CommandServiceFixture.cs
Update Position references to Revision                                     
+1/-1     
InMemoryEventStore.cs
Update Position references to Revision                                     
+1/-1     
SubscribeToStream.cs
Update Position references to Revision                                     
+1/-1     
Tests
2 files
AggregateSpec.cs
Pass version when loading test aggregate                                 
+2/-1     
AppServiceTests.cs
Add test for stream deletion scenario                                       
+18/-1   

* Rename Position to Revision in StreamEvent for clarify
@qodo-free-for-open-source-projects
Copy link
Contributor

qodo-free-for-open-source-projects bot commented Nov 23, 2025

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🟡
🎫 #412
🟢 Support aggregates with streams starting from non-zero positions after stream deletion
Fix `Aggregate.OriginalVersion` calculation to handle deleted and recreated streams
Prevent `OptimisticConcurrencyException` when executing commands on recreated streams
Allow aggregate to continue functioning normally after stream deletion and recreation
First command after stream deletion should succeed
Subsequent commands after stream deletion should succeed without version conflicts
Codebase Duplication Compliance
🟢
No codebase code duplication found No new components were introduced in the PR code
Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Missing null validation: The Load method accepts IEnumerable<object?> with nullable objects but only filters
nulls without validating the version parameter for negative values beyond initialization

Referred Code
public void Load(long version, IEnumerable<object?> events) {
    Original        = events.Where(x => x != null).ToArray()!;
    OriginalVersion = version;
    State           = Original.Aggregate(State, Fold);

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Unvalidated version parameter: The Load method accepts a version parameter without validation, which could lead to
incorrect state if negative or invalid values are passed

Referred Code
public void Load(long version, IEnumerable<object?> events) {
    Original        = events.Where(x => x != null).ToArray()!;
    OriginalVersion = version;

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-free-for-open-source-projects
Copy link
Contributor

qodo-free-for-open-source-projects bot commented Nov 23, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Prevent exception on empty event stream
Suggestion Impact:The suggestion was implemented with a slight variation. Instead of wrapping the aggregate.Load() call in an if block, the commit added an early return statement when events.Length == 0, which achieves the same goal of preventing access to an empty array while being more concise.

code diff:

+                if (events.Length == 0) return aggregate;

Add a check to ensure the events array is not empty before accessing its last
element to prevent a potential IndexOutOfRangeException.

src/Core/src/Eventuous.Persistence/AggregateStore/AggregatePersistenceExtensions.cs [130-131]

 var events = await eventReader.ReadStream(streamName, StreamReadPosition.Start, failIfNotFound, cancellationToken).NoContext();
-aggregate.Load(events[^1].Revision, events.Select(x => x.Payload));
 
+if (events.Length > 0) {
+    aggregate.Load(events[^1].Revision, events.Select(x => x.Payload));
+}
+

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a potential IndexOutOfRangeException when an existing stream is empty and proposes a valid fix, improving the code's robustness.

Medium
General
Reuse existing ID object in test

In the ProcessNewThenDeleteAndDoItAgain test, reuse the existing cmd.BookingId
object instead of creating a new one for the cancelCmd to improve clarity and
safety.

src/KurrentDB/test/Eventuous.Tests.KurrentDB/AppServiceTests.cs [42-57]

 [Test]
 public async Task ProcessNewThenDeleteAndDoItAgain(CancellationToken cancellationToken) {
     // This will create a new stream
     var cmd = DomainFixture.CreateImportBooking();
     await Service.Handle(cmd, cancellationToken);
 
     var streamName = StreamName.For<Booking>(cmd.BookingId);
     await _fixture.EventStore.DeleteStream(streamName, ExpectedStreamVersion.Any, cancellationToken);
 
     var handlingResult = await Service.Handle(cmd, cancellationToken);
     handlingResult.Success.ShouldBeTrue();
 
-    var cancelCmd    = new Commands.CancelBooking(new(cmd.BookingId));
+    var cancelCmd    = new Commands.CancelBooking(cmd.BookingId);
     var secondResult = await Service.Handle(cancelCmd, cancellationToken);
     secondResult.Success.ShouldBeTrue();
 }

[Suggestion processed]

Suggestion importance[1-10]: 4

__

Why: The suggestion is correct that reusing the cmd.BookingId object is cleaner, but since BookingId is a record, new(cmd.BookingId) creates an equal object, so the current test code is functionally correct.

Low
  • Update

Comment on lines +53 to +55

var cancelCmd = new Commands.CancelBooking(new(cmd.BookingId));
var secondResult = await Service.Handle(cancelCmd, cancellationToken);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Reuse existing ID object in test

Suggested change
var cancelCmd = new Commands.CancelBooking(new(cmd.BookingId));
var secondResult = await Service.Handle(cancelCmd, cancellationToken);
var cancelCmd = new Commands.CancelBooking(cmd.BookingId);
var secondResult = await Service.Handle(cancelCmd, cancellationToken);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't work because cmd.BookingId is a string but CancelBooking constructor expects a BookingId instance which has no implicit conversion from string.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question:

This won't work because cmd.BookingId is a string but CancelBooking constructor expects a BookingId instance which has no implicit conversion from string.

Answer:

The user has identified a type mismatch issue where the AI suggested using cmd.BookingId (a string) directly in a constructor that expects a BookingId object instance. This is a case where the AI made an incorrect assumption about the code structure.

To help prevent similar issues in future suggestions, you can add specific guidance to the extra_instructions configuration:

[pr_code_suggestions]
extra_instructions = """
- When suggesting code changes involving domain objects or value objects, ensure type compatibility
- Do not assume implicit conversions between primitive types (like string) and domain objects
- If a constructor requires a specific type instance, verify the variable type matches before suggesting its use
"""

However, it's important to note that this represents an inherent limitation of AI code analysis. The model may not always have complete context about custom type systems, domain objects, and their conversion rules in your codebase. While the extra_instructions can help guide the model toward better type awareness, it cannot guarantee the model will always correctly identify type mismatches in complex domain models.

For C# codebases with strong typing and domain-driven design patterns, you might also consider:

[pr_code_suggestions]
extra_instructions = """
- Respect strong typing in C# - do not suggest implicit conversions that don't exist
- When working with value objects or domain entities, verify constructor signatures match the provided arguments
"""

This type of error is relatively rare but can occur when the AI lacks full context about your type system. The suggestions should still be reviewed critically, as recommended in the Understanding AI Code Suggestions documentation.

Relevant Sources:

@github-actions
Copy link

github-actions bot commented Nov 23, 2025

Test Results

 51 files  + 34   51 suites  +34   32m 0s ⏱️ + 21m 5s
278 tests +  9  278 ✅ +  9  0 💤 ±0  0 ❌ ±0 
843 runs  +563  843 ✅ +563  0 💤 ±0  0 ❌ ±0 

Results for commit 4868104. ± Comparison against base commit 05689c4.

This pull request removes 5 and adds 14 tests. Note that renamed tests count towards both.
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(11/22/2025 2:41:35 PM +00:00)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(11/22/2025 2:41:35 PM)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(393180c3-a2a7-4de1-bf68-874ac08b21e2)
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2025-11-22T14:41:35.8604159+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2025-11-22T14:41:35.8604159+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2025-11-22T14:41:35.8604159+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2025-11-22T14:41:35.8604159+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2025-11-22T14:41:35.8604159+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2025-11-22T14:41:35.8604159+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2025-11-22T14:41:35.8604159+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2025-11-22T14:41:35.8604159+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2025-11-22T14:41:35.8604159+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2025-11-22T14:41:35.8604159+00:00 })
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(11/23/2025 8:01:54 PM +00:00)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(11/23/2025 8:01:54 PM)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(11/23/2025 8:02:02 PM +00:00)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(11/23/2025 8:02:02 PM)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(5b6cc6a3-6b08-47a8-9da8-55065028c8a5)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(6eed2c82-a436-4732-b88d-18f4b3c7cd60)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(abf702ee-ac02-4c00-82b9-7611aa241069)
Eventuous.Tests.KurrentDB.AppServiceTests ‑ ProcessNewThenDeleteAndDoItAgain
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2025-11-23T20:01:54.6484151+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2025-11-23T20:01:54.6484151+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2025-11-23T20:01:54.6484151+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2025-11-23T20:01:54.6484151+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2025-11-23T20:01:54.6484151+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2025-11-23T20:01:54.6484151+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2025-11-23T20:01:54.6484151+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2025-11-23T20:01:54.6484151+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2025-11-23T20:01:54.6484151+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2025-11-23T20:01:54.6484151+00:00 })
…

♻️ This comment has been updated with latest results.

@alexeyzimarev alexeyzimarev merged commit 374d9a8 into dev Nov 23, 2025
5 checks passed
@alexeyzimarev alexeyzimarev deleted the aggregate-version-fix branch November 23, 2025 20:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Second command to aggregate with deleted stream will fail

2 participants